Skip to content

Feature: Improve IP / Port Scanner and various fixes / improvements - #3660

Merged
mergify[bot] merged 147 commits into
mainfrom
feature/improve-scanners-2
Oct 28, 2026
Merged

mergify[bot] merged 147 commits into
mainfrom
feature/improve-scanners-2

Conversation

@BornToBeRoot

@BornToBeRoot BornToBeRoot commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Changes proposed in this pull request

  • See commits

Related issue(s)

To-Do

Contributing

By submitting this pull request, I confirm the following:

BornToBeRoot and others added 30 commits October 24, 2026 12:54
Replace the positional constructors (14 / 6 parameters) of IPScannerOptions
and PortScannerOptions with init-only properties set via object initializer.
No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-host lambda in ScanAsync is split into DiscoverAsync (ping, port scan
and NetBIOS concurrently -> reachable) and EnrichAsync (DNS PTR, MAC address
and vendor -> result), with ResolveHostnameAsync / ResolveMACAddressAsync as
own methods. Prepares progressive results (enrich as soon as a host answers).
No behaviour change. Port Scanner: nothing to split (per port probe -> result,
PTR already runs concurrently).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PTR record is fully qualified (e.g. "server.example.com."). The IP Scanner
only trimmed it for custom commands and "Add profile", so the grid, the export
and other features (DNS Lookup, Remote Desktop, ...) got the hostname with the
trailing dot. It is now trimmed once when the result is created, in the IP
Scanner and the Port Scanner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DNS hostname (without the NetBIOS fallback of Hostname) was only exported
to JSON. Add it to CSV and XML, right after Hostname (JSON moved to the same
position). Port Scanner: no separate DNS hostname, nothing to change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n scan (C5)

Columns, Copy menu entries and the row details (IP Scanner) were bound to the
current settings. After changing a setting the shown results got columns
without data (e.g. MAC address enabled after the scan) or lost columns with
data, the row details showed "Port scan is disabled" for scanned ports.

The view models keep the options of the shown scan (ScanOptions, until the
first scan the settings when the tab was opened) and the views bind to them
via the BindingProxy (DataGrid columns are not in the visual tree).
Port Scanner: Hostname column and Copy > Hostname.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ner)

Split the combined "IP Scanner / Port Scanner" section and the "also in the
..." / "the same applies to ..." remarks into separate entries in the section
of each tool (IP Scanner, Port Scanner, Ping Monitor, Traceroute).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trailing dot was only trimmed in the IP Scanner and Port Scanner (C2).
Ping Monitor, Traceroute, Connections and the Network Connection widget still
showed "server.example.com.". Trim it once in DNSClient.ResolvePtrAsync
instead and remove the per-tool trims (IP Scanner, Port Scanner, Ping Monitor
title / notification).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trimming the trailing dot turned a single-label PTR name like "gateway." into
"gateway". DNSClientHelper.IsBareHostname treats that as a bare hostname, so
with "Add DNS suffix" enabled, DNS Lookup and every other tool that resolves
it (e.g. when opened from the IP Scanner) queried "gateway.<suffix>" instead.

New DNSClientHelper.TrimTrailingDot only trims fully qualified names with more
than one label, used by DNSClient.ResolvePtrAsync. Unit tests added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since C5 the columns, Copy menu and row details were a snapshot of the
settings when the tab was opened until the first scan - before, they followed
the settings live. While no results are shown (and no scan is running), the
view models now refresh ScanOptions when a setting that affects them changes
(IP Scanner: resolve hostname, port scan, NetBIOS, MAC address; Port Scanner:
resolve hostname). The snapshot no longer allocates the ICMP buffer (up to
65500 bytes per tab), it is only created for a scan. The settings handler is
removed when the tab is closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With init properties (R1) a forgotten option silently became 0 / false
(e.g. MaxHostThreads = 0 -> Parallel.ForEachAsync throws), the positional
constructor caught this at compile time. Mark all options of IPScannerOptions
and PortScannerOptions as required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C4 inserted the DNSHostname column after Hostname, which moved every following
column by one - scripts / spreadsheets that read the CSV by position would read
the wrong values. Add it as last column instead (XML and JSON are read by name
and keep it after Hostname).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IsReachable was a computed property that went through the port list again on
every read (about 4 times per host). Compute it once in the initializer like
IsAnyPortOpen. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ently (review #7)

EnrichAsync ran the PTR lookup and the MAC address lookup (neighbor table,
local interfaces, OUI) one after the other, although they don't depend on each
other. Start both and await them together - a host is finished after the
slower one instead of after both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew A)

Since the PTR name keeps the dot of a single-label name (review #1, so other
tools don't append the DNS suffix), the removed per-tool trims (C2, review #3)
made custom commands ({{Hostname}}), the profile name of "Add profile" and the
Ping Monitor title / notification use "gateway." instead of "gateway" (main).
For custom commands this is more than cosmetic: Windows resolves a name with a
trailing dot via DNS only (no suffix search, no LLMNR / NetBIOS).

Trim the dot again in these places (display / external programs); the value
passed to other NETworkManager tools keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ScanOptions kept the full IPScannerOptions of the last scan for the lifetime
of the tab (ports array, ICMP buffer - up to ~320 KB for 1-65535 / 65500
bytes) only to read four flags, and CreateScanOptions(null) created options
for display that claimed a port scan without ports.

New IPScannerResultOptions record with the four flags the view binds to
(hostname, port scan, NetBIOS, MAC address), created from the options of the
scan or from the settings. CreateScanOptions is only used for a scan again.
Port Scanner unchanged: PortScannerOptions only holds six scalar values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ScanOptions only followed the settings if a setting changed while no results
were shown. An empty result could still keep the columns of the previous scan:
Results.Clear() ran long before the options of the new scan were set, and a
scan that was canceled during the preparation or found nothing didn't update
them either. The handler also listed the relevant settings by hand (easy to
miss when a column is added).

UpdateScanOptionsIfNoResults is called after the results are cleared, when a
scan is complete and on any settings change (IP Scanner + Port Scanner).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew C)

IPScanner and PortScanner had an identical private ResolveHostnameAsync (PTR
lookup, stop waiting on cancellation, empty string on error). Replace both
with DNSClient.ResolvePtrOrEmptyAsync. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HostDiscovery caches IsAnyPortOpen / IsReachable at construction. As a
positional record, a "with" expression would copy these cached values instead
of recomputing them, and the generated equality / ToString members are unused.
Use a private sealed class with read-only properties. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EnrichAsync checked discovery.NetBIOSInfo with ?. three times, but it is never
null (an empty NetBIOSInfo if NetBIOS is disabled or there was no answer) and
HostDiscovery.IsReachable already dereferences it. Keep the ?? fallbacks for
its values, which may be null. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CSV export got DNSHostname as last column (review #5), but the XML export
still inserted it after Hostname (moving the following elements) and the JSON
export moved the existing property next to Hostname. XML: last element, JSON:
back to its previous position - no existing column / element / property
changes its position compared to the last release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3, step 23)

IPScannerHostInfo is observable (PropertyChangedBase, setters internal to the
model). Hostname / MACAddress / Vendor are computed from their sources (DNS ->
NetBIOS, neighbor table -> NetBIOS), so a value that arrives later updates them.

The scanner raises HostFound right after the discovery (ping, port scan,
NetBIOS) and the DNS hostname and MAC address / vendor as HostUpdated when they
arrive (replaces HostScanned). The scanner never changes a host itself: the
view model buffers HostFound and HostUpdated in one list (order kept, an update
always after its host) and applies them on the UI thread in the existing
150 ms flush.

A host is only finished (progress) when all its lookups are finished, and
ScanComplete is raised only after every host body returned - the view model
flushes the remaining updates before the scan ends. Events of a scanner other
than the running one are ignored and logged (guard only, can't happen).

Export copies the rows on the UI thread (Clone), since they may still change
during a scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…4, P9)

ScanHostAsync replaces DiscoverAsync / HostDiscovery: ping, port scan and
NetBIOS run concurrently and are processed as they finish (Task.WhenAny). The
port scan signals the first answering port (open or refused) right away. On the
first answer the host is raised in HostFound - with placeholders for the probes
that are still running (IsPingPending / IsPortScanPending / IsNetBIOSPending) -
counted as up, and the DNS hostname and MAC address lookups start (P9). Every
probe that finishes afterwards is raised as HostUpdated. Unreachable hosts are
counted / raised (show unreachable) when all probes are finished.

A host still only finishes (progress, and the scan with it) when every probe and
lookup is finished: a finally waits for all of them, also when a probe was
canceled or failed.

View: Ping status, Port status and NetBIOS reachable show "..." while their
probe is running (new PendingToStringConverter); the ping details and "No open
ports found" are hidden until the ping / port scan is finished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The view models stored the progress value of the ProgressChanged event args.
Events of different hosts / ports raised at the same time can arrive in a
different order, so an older (smaller) value could overwrite a newer one - also
the last one, so the result could end with e.g. 255 of 256 hosts scanned. Step
24 raises ProgressChanged also when a host is found, which makes this more
likely.

The scanners expose HostsScanned / PortsScanned; the view models read all
counters from the scanner on every event and once more in ScanComplete (final
values).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New Time column after Ping status with the round trip time of the ping reply
("-/-" without a reply, "..." while the ping is running - optional third value
of PingTimeToStringConverter, existing users unchanged). Sorting by time uses a
custom comparer: hosts without a ping reply (time 0) are always last, in both
directions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tools (phase 3, step 27, C1)

The hostname shown in the result falls back to the NetBIOS computer name. That
name was passed to other tools (Port Scanner, Remote Desktop, PuTTY, ...) and to
DNS Lookup / RDAP > Hostname, where it may not be resolvable via DNS. Other
tools now get the DNS hostname, otherwise the IP address; DNS lookup / RDAP >
Hostname are only enabled with a DNS hostname. Custom commands keep
{{Hostname}} = shown hostname (documented).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ep 28, C3)

The context menu for multiple selected results only had Export. New Copy
submenu (same strings and style as the single-row Copy menu), one value per
line in the order shown in the grid, without duplicates:
- IP Scanner: IP address, Hostname, MAC address (visibility like the single-row
  menu / the columns of the shown scan)
- Port Scanner: IP address, Hostname, Port, IP address:Port (IPEndPoint format,
  IPv6 in brackets)

The grid order helper is shared with the export (GetSelectedResultsInGridOrder).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Search box right of the Result header (SearchTextBox, width 250, delayed filter
like the Firewall - GlobalStaticConfiguration.SearchDispatcherTimerTimeSpan):
- IP Scanner: IP address, hostname, MAC address, vendor, shown ports (number or
  service). Live filtering on Hostname / MACAddress / Vendor / Ports, so a host
  found before its hostname or MAC address arrived is filtered again.
- Port Scanner: IP address, hostname, port, protocol, service, description,
  status (also translated). Results don't change after they were added.

Enter in the search box filters right away (ApplySearch) and is handled, so it
doesn't trigger the scan button (IsDefault) and clear the results. Counters
always count all hosts / ports; Export > all exports the filtered results
(documented).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le (phase 3, step 30, M2)

IP Scanner context menu:
- Web Console: opens the host (DNS hostname, otherwise the IP address - C1) via
  https if 443 is open, otherwise the first open of 80 / 8443 / 8080, http on
  the default port if none is open or the port scan is still running/disabled.
- Wake on LAN: sends the magic packet to the MAC address (WakeOnLANRequest),
  only enabled with a MAC address.
Port Scanner: Web Console for an open web port (80, 443, 8080, 8443).

New WebConsoleUrlHelper (URL from host + ports: scheme, default ports left out,
IPv6 in brackets with an encoded zone id) with unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ep 31, M4)

Context menu Rescan (single and multiple rows, disabled while a scan runs), new
string "Rescan".

IP Scanner: scans the IP addresses of the selected rows again with the options
of the shown scan (kept as _lastScanOptions, so the columns stay valid) and new
option ReportAllHosts (every host is raised, also if unreachable now, without
showing all ports like ShowAllResults). The scanner creates new results; their
values are copied into the existing rows (IPScannerHostInfo.CopyFrom, selection
and expanded details stay) and their updates are applied to these rows
(IPScannerHostUpdatedArgs.ApplyTo). Rows are matched by the new scanned
IPAddress of IPScannerHostInfo (PingInfo has the reply address). Up / down are
counted from the rows during the rescan, the progress of the last scan is
restored afterwards.

Port Scanner: the rows of the selected hosts are removed and scanned again with
the ports and options of the last scan; open / closed = counters of the other
hosts + the ones of the rescan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g Monitor (phase 3, step 33, T1)

The counters were built from the number and a separate word ("{0} {1}" with
Strings.Up / Down / Open / Closed / Paused), so translations couldn't change
the word order. New strings XUp / XDown / XOpen / XClosed / XPaused ("{0} Up"
etc., same English text) used as StringFormat in the IP Scanner and Port
Scanner and as format of PingMonitorGroupSummaryConverter (Ping Monitor group
header). The old strings stay (used elsewhere).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BornToBeRoot and others added 26 commits October 27, 2026 22:57
…, its target is a URL (final final review #4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k to the IP address (final final review #5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dot of the hostname (final final review #6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew #7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al review #8)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t after resolving the hostnames (B8)

Only "The following hostnames could not be resolved" is shown instead of also
"No reachable hosts found" / "No open ports found", and the input is not added
to the history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot only on an upgrade

A settings file edited (or restored) after the upgrade to the current version
kept a buffer of e.g. 65535, which makes .NET's Ping throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tings, not only on an upgrade"

This reverts commit a47e4b1. The buffer is lowered inline in UpgradeToLatest again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idths in a loop and hang the window

DataGridHelper.FillRemainingWidth: a horizontal scrollbar can make the vertical one
appear, the narrower fill column removes both, the wider one shows the horizontal
scrollbar again ... Each update is queued with Loaded priority (before input), so
the window hangs without an exception. Going back to the wider width of two updates
ago within 500 ms (without a size change of the grid) keeps the narrower width.

Seen once in a Port Scanner run 1-65535 (stuck in the middle, no exception);
cause not confirmed with a stack trace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The custom commands grid allowed selecting several rows, but Edit / Delete only
used one of them. Like the profiles: MultiSelectDataGrid with the selected
commands, a context menu with Delete for several rows, Del deletes all selected
after a confirmation that lists them, Edit (F2, context menu) only with exactly
one selected command. New strings DeleteCustomCommands / DeleteCustomCommandsMessage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad of Status (breaking, PS-C3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at once

Like the custom commands of the IP Scanner: MultiSelectDataGrid, context menu with
Delete for several rows, Del deletes all selected after a confirmation that lists
them, Edit only with exactly one. The first port profile is still selected on
open and after deleting. New strings DeletePortProfiles / DeletePortProfilesMessage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, Computer Management, Event Viewer and System Information

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ost in the group header

Rescan in the row context menu rescanned all ports of the selected hosts (e.g. all
65535 to check one port). Now:
- Rows: Rescan scans only the selected ports (PortScanner.ScanPortsAsync, per host
  only its ports) and updates their state in place (PortInfo.State observable);
  selection, collapsed groups and the scroll position stay. Every rescanned port is
  reported (a port closed now keeps its row with the new state, like a host down
  now in the IP Scanner), without ping before the scan and hostname resolution.
  Counters from base + rows (also after a cancel), closed / timed out per host and
  the open count of the group header updated (OpenPortsPerHostVersion).
- Group header: context menu Rescan host - all ports of the shown scan, rows
  replaced (previous host rescan).
PortScannerOptions is a record (with for the rescan options). New string RescanHost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…faster with all hosts expanded)

Every expanded row created 3 ListViews with a MahApps ListViewItem (own Grid,
selection / hover triggers) per value and per port - all transparent, nothing
selectable. Ping details and NetBIOS are one Grid with rows now, the ports an
ItemsControl with the same item template. Same layout (min. row height 25 like the
list items), the values are still TextBoxes (selectable, Copy context menu).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the visible part can be scrolled into

ScrollUnit Item scrolled by whole rows: a partly visible expanded row (row details,
e.g. with Expand host view) could only be skipped, not scrolled into. Virtualization
(Recycling) stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ctedItem})

InvalidCastException 'MS.Internal.NamedObject' to IPStatus: a recycled row
container (virtualization, more often with pixel scrolling and expanded rows) passes
{DisconnectedItem} / UnsetValue to the MultiBinding. The converter cast the values
without a type check; returns an empty string for other values now. Used by the IP
Scanner (Time column, row details) and Traceroute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ewItem"

This reverts commit 0394067. No noticeable difference with all hosts expanded in
the manual test, the tested layout is kept. Pixel scrolling (17434ef) and the
converter fix (c163bae) stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Info became observable

Type-qualified bindings ((network:PortScannerPortInfo.State) etc.) pass no property
name to PropertyChangedEventManager.AddHandler for an INotifyPropertyChanged source
(PortInfo since b740659) -> ArgumentNullException 'key'. Plain paths like the IP
Scanner row details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… closed ports shown)

Break All: UI thread in the CustomSort comparer from AddResult / FlushResultsBuffer.
WPF inserts every added row into its group of the sorted, grouped view with a linear
search of the group (CollectionViewGroupInternal.FindIndex) - with all 65535 ports
of a host in one group about 2 billion comparisons on the UI thread. Like the rescan
(ReplaceResults): above MaxRowChangesOneByOne (added rows x all rows) the buffered
rows are added with one reset (sorted and grouped once).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… results were added

ArgumentException "Width and Height must be non-negative" from
VirtualizingStackPanel.SyncUniformSizeFlags (WPF, virtualization with grouping and
collapsed groups) while a row was added during a scan - also on main. The row is
already in the results when the notification throws; the remaining rows (rescan:
all rows of the hosts) are added with one reset, which rebuilds the view. Only this
exception (thrown by the Size constructor) is caught, logged as warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, Add profile and Copy > Open ports

The group header menu (Rescan host) also has Ping Monitor, Traceroute, DNS lookup,
RDAP, Remote Desktop, PowerShell, PuTTY, TigerVNC, SNMP, Add profile... and Copy >
IP address / Hostname / Open ports (e.g. "22; 80; 443", only with an open port).
Port-specific items (Web Console, port / state / service) stay in the row menu.

The commands take the group as an optional parameter (GetHostRow: first row of the
group, otherwise the selected result), so a right-click on a header doesn't change
the selection. Open in tool: [application, group] via the new
MultiValueToArrayConverter. New string OpenPorts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…igh as the input

Same style as the resolve button of other inputs (size bound to the TextBox height).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ested creating it with the same name

The profile refers to port profiles / DNS servers by their internal ID, a new one
with the same name is not found. The message says to create it and then select it in
the profile again (edit the profile).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the scan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mergify
mergify Bot merged commit 62fc57f into main Oct 28, 2026
5 checks passed
@mergify
mergify Bot deleted the feature/improve-scanners-2 branch October 28, 2026 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IP Scanner: Resizeable columns

2 participants